import * as vscode from 'vscode'
import Git, {
  EOL_REGEX,
  GitBranches,
  GitFileContentFromCommitOptions,
  GitFileLogFromCommitRangeOptions,
  GitFilesFromCommitsOptions,
} from '../git/Git'

export interface BlameLineItem {
  inCommits: boolean
  startLine: number
  stepLine: number
  blame: {
    author: string
    mail: string
    time: Date
    summary: string
    hash: string
  }
}

export type BlameLineData = BlameLineItem[]

export class InlineGit extends Git {
  async cmd(...args: string[]) {
    return vscode.window.withProgress(
      { location: { viewId: 'reviewMycode.sidebar' } },
      async () => {
        try {
          return await super.cmd(...args)
        } catch (ex) {
          vscode.window.showErrorMessage((ex as Error).message)
          throw ex
        }
      }
    )
  }

  async getDiffFiles(opts: GitFilesFromCommitsOptions) {
    if (opts.author) {
      opts.authors = opts.author.split(/\s+/)
      delete opts.author
    }
    const [realDiffFilesMap, filesMap] = await Promise.all([
      this.getFilesDiffFromCommits(opts),
      this.getFilesFromCommits(opts),
    ])

    const files = []
    for (const [filePath, item] of realDiffFilesMap) {
      const authors = filesMap.get(filePath)
      if (authors) {
        files.push({
          ...item,
          authors,
        })
      }
    }

    return files
  }

  async getCommitHashs(opts: GitFilesFromCommitsOptions) {
    if (opts.author) {
      opts.authors = opts.author.split(/\s+/)
      delete opts.author
    }
    return await this.getCommitsFromCommits(opts)
  }

  async getBranchList() {
    const { branches } = await this.getBranches()
    return branches.sort()
  }

  async getDiffAddLineNum(opts: GitFileLogFromCommitRangeOptions) {
    const stdout = await this.cmd('diff', opts.commitIdStart, opts.commitIdEnd, '--', opts.filePath)
    const res: Set<number> = new Set()

    const REG = /\n@@ .\d+,\d+ .(\d+),\d+ @@[^\n]*\n/g
    let matchList = REG.exec(stdout)
    do {
      if (!matchList) break
      const [, _startLine] = matchList
      const startLine = +_startLine
      const startIndex = REG.lastIndex

      matchList = REG.exec(stdout)
      const endIndex = matchList ? REG.lastIndex - matchList[0].length : stdout.length

      let i = 0
      stdout
        .slice(startIndex, endIndex)
        .split(EOL_REGEX)
        .forEach((v) => {
          const q = v[0]
          if (q !== '-') {
            if (q === '+') res.add(startLine + i)
            ++i
          }
        })
    } while (true)
    return res
  }

  async getBlameFileIncremental(opts: GitFileContentFromCommitOptions) {
    const stdout = await this.cmd('blame', opts.commit, '--incremental', '--', opts.filePath)
    return stdout
  }

  async getBlameInDiffAndCommits(
    opts: GitFileLogFromCommitRangeOptions,
    commitSet: Set<string>
  ): Promise<BlameLineData> {
    const [blameFileContent, addLineNumSet] = await Promise.all([
      this.getBlameFileIncremental({
        filePath: opts.filePath,
        commit: opts.commitIdEnd,
      }),
      this.getDiffAddLineNum({
        filePath: opts.filePath,
        commitIdStart: opts.commitIdStart,
        commitIdEnd: opts.commitIdEnd,
      }),
    ])

    const res: BlameLineData = []

    const blameTool = {
      hashToTextMap: new Map<
        string,
        { startIndex: number; endIndex: number; blame?: BlameLineItem['blame'] }
      >(),
      saveMeta(hash: string, startIndex: number, endIndex: number) {
        if (!this.hashToTextMap.has(hash)) this.hashToTextMap.set(hash, { startIndex, endIndex })
      },
      getBlame(hash: string) {
        const v = this.hashToTextMap.get(hash)
        if (!v) throw new Error('not found hash')
        if (!v.blame) {
          const contentLines = blameFileContent.slice(v.startIndex, v.endIndex).split(EOL_REGEX)
          const res: { [key: string]: string } = {}
          const SPLIT_REG = /^([^\s]+) (.*)/
          contentLines.forEach((v) => {
            const m = v.match(SPLIT_REG)
            if (m) {
              const [, key, value] = m
              res[key] = value
            }
          })
          v.blame = {
            author: res.author,
            mail: res['author-mail']?.slice(1, -1) ?? '',
            time: new Date(+((res['author-time'] ?? '0000000000') + '000')),
            summary: res.summary,
            hash,
          }
        }
        return v.blame
      },
    }

    const HASH_START_REG = /(?:\n|^)(\w{40}) (\d+) (\d+) (\d+)[^\n]*\n?/g
    let matchList = HASH_START_REG.exec(blameFileContent)
    do {
      if (!matchList) break
      const startIndex = HASH_START_REG.lastIndex

      const [, hash, _prevStartNum, _startNo, _step] = matchList
      const startNo = +_startNo
      const step = +_step

      matchList = HASH_START_REG.exec(blameFileContent)
      const endIndex = matchList
        ? HASH_START_REG.lastIndex - matchList[0].length
        : blameFileContent.length

      blameTool.saveMeta(hash, startIndex, endIndex)

      if (commitSet.has(hash)) {
        res.push({
          inCommits: true,
          startLine: startNo,
          stepLine: step,
          blame: blameTool.getBlame(hash),
        })
      } else {
        let blame: BlameLineItem['blame'] | null = null
        let startLineNo: number | null = null
        let endLineNo: number | null = null
        function push() {
          if (startLineNo && endLineNo) {
            if (!blame) blame = blameTool.getBlame(hash)
            res.push({
              inCommits: false,
              startLine: startLineNo,
              stepLine: endLineNo - startLineNo + 1,
              blame,
            })
            startLineNo = endLineNo = null
          }
        }
        for (let i = 0; i < step; ++i) {
          const lineNo = startNo + i
          if (addLineNumSet.has(lineNo)) {
            if (!startLineNo) {
              startLineNo = endLineNo = lineNo
            } else {
              endLineNo = lineNo
            }
          } else {
            push()
          }
        }
        push()
      }
    } while (true)
    return res.sort((a, b) => a.startLine - b.startLine)
  }
}
